package org.hawk.gwt.ppc.scanner.utils;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.hawk.gwt.ppc.javacc.ParseException;
import org.hawk.gwt.ppc.reflect.lang.SystemClassLoader;
import org.hawk.gwt.ppc.scanner.SourcesLoader;
import org.hawk.gwt.ppc.scanner.Token;
import org.hawk.gwt.ppc.scanner.TokenChain;
import org.hawk.gwt.ppc.scanner.UniversalSourcesLoader;
import org.hawk.gwt.ppc.utils.Name;
import org.hawk.gwt.ppc.utils.StringUtils;

/**
 * Utility class containing all methods to read and 
 * parse class sources and to operate results of these operations.
 * Also has logic to cache the results of parsing.
 * @author alex.bereznevatiy@gmail.com
 */
public final class JavaClassDescriptorUtils {
	private JavaClassDescriptorUtils(){}
	
	private static Map<SourcesLoader,Map<String,
		SoftReference<JavaClassDescriptor>>> loaded = 
						new HashMap<SourcesLoader, Map<String,
							SoftReference<JavaClassDescriptor>>>();
	
	/**
	 * Creates class descriptor based on runtime class.
	 * This method is useful for classes that has no sources. 
	 * @param cls class to describe
	 * @return JavaClassDescriptor for class specified.
	 * @throws IOException in case of IO problems
	 */
	public static JavaClassDescriptor getClassDescriptor(Class<?> cls) {
		if (cls == null) {
			throw new NullPointerException("Class is NULL");
		}
		if (loaded.get(null) == null) {
			loaded.put(null, new HashMap<String, SoftReference<JavaClassDescriptor>>());
		}
		SoftReference<JavaClassDescriptor> ref = loaded.get(null).get(cls.getName());
		JavaClassDescriptor result = null;
		if (ref != null) {
			result = ref.get();
		}
		if (result == null) {
			result = new JavaClassDescriptor(cls);
			loaded.get(null).put(cls.getName(), new SoftReference<JavaClassDescriptor>(result));
		}
		return result;
	}
	/**
	 * @param className
	 * @return JavaClassDescriptor for class with name specified.
	 * Source will be loaded with default sources loader (using 
	 * java class path).
	 * @throws IOException in case of IO problems
	 * @throws ParseException in case if parsing fails
	 */
	public static JavaClassDescriptor getClassDescriptor(String className) 
			throws IOException, ParseException {
		return getClassDescriptor(className, new UniversalSourcesLoader());
	}
	/**
	 * 
	 * @param className
	 * @param loader
	 * @return JavaClassDescriptor for class with name specified.
	 * Source will be loaded with sources loader from the second parameter.
	 * @throws IOException in case of IO problems
	 * @throws ParseException in case if parsing fails
	 */
	public static JavaClassDescriptor getClassDescriptor(String className,
			SourcesLoader loader) throws ParseException, IOException{
		className = Name.getRawName(className);
		if(loaded.get(loader)==null){
			loaded.put(loader, new HashMap<String, 
					SoftReference<JavaClassDescriptor>>());
		}
		SoftReference<JavaClassDescriptor> ref = loaded.get(loader).get(className);
		JavaClassDescriptor descr = null;
		if(ref != null){
			descr = ref.get();
		}
		if (descr == null) {
			descr = createDescriptor(className, loader);
		}
		if (descr == null) {
			throw new IOException("Unable to load class with name " + className);
		}
		return descr;
	}
	
	private static JavaClassDescriptor createDescriptor(String className, SourcesLoader loader) throws ParseException, IOException {
		if (loader.isClassExists(Name.getPureClassName(className))) {
			JavaClassDescriptor descr = new JavaClassDescriptor(className, loader);
			loaded.get(loader).put(className, new SoftReference<JavaClassDescriptor>(descr));
			return descr;
		}
		try {
			return getClassDescriptor(Class.forName(className));
		} catch (ClassNotFoundException ex) {
			return null;
		}
	}
	/**
	 * Checks the expression passed for the result type.
	 * This means that whole expression will be evaluated
	 * and corresponding type will be 100% guaranteed.
	 * 
	 * Note that expression should be valid. This means that 
	 * it should begins with valid java identifier and ends with
	 * semicolon or dot. General requirement is - expression 
	 * should be compilable (this all the methods and variables 
	 * should be declared and there are no syntax errors neither
	 * in current class nor in referred ones). 
	 * 
	 * Also note that iterator passed will not be changed and will
	 * point at the same index as before calling this method.
	 * 
	 * @param iterator to read the expression
	 * @param className name of the class declaring this expression
	 * @param start including index of expression. Should point on 
	 * start of valid java identifier.
	 * @param end excluding index of expression. Should point either
	 * on dot (.) or semicolon (;). 
	 * @return full name of the return type of this expression. 
	 * If expression doesn't returns any value (return type is void) 
	 * - <code>null</code> will be returned as a result.
	 * If return type contains some type parameters - they also will
	 * be returned in java manner (using triangle brackets) 
	 * @throws IOException in case of IO problems (unable to read sources file,
	 * unable to find source, etc.)
	 * @throws ParseException in case of either expression is 
	 * invalid or either current class or related ones have 
	 * syntax error(s).
	 */
	public static String evaluateReturnTypeOfExpression(
			TokenChain<? extends Token> iterator,
			String className,int start,int end) throws ParseException, IOException{
		return evaluateReturnTypeOfExpression(iterator,className,null,start,end);
	}
	/**
	 * 
	 * Checks the expression passed for the result type.
	 * This means that whole expression will be evaluated
	 * and corresponding type will be 100% guaranteed.
	 * 
	 * Note that expression should be valid. This means that 
	 * it should begins with valid java identifier and ends with
	 * semicolon or dot. General requirement is - expression 
	 * should be compilable (this all the methods and variables 
	 * should be declared and there are no syntax errors neither
	 * in current class nor in referred ones). 
	 * 
	 * Also note that iterator passed will not be changed and will
	 * point at the same index as before calling this method.
	 * 
	 * @param iterator to read the expression
	 * @param className name of the class declaring this expression
	 * @param loader sources loader of the java sources for the class 
	 * with name from the previous parameter.
	 * @param start including index of expression. Should point on 
	 * start of valid java identifier.
	 * @param end excluding index of expression. Should point either
	 * on dot (.) or semicolon (;). 
	 * @return full name of the return type of this expression. 
	 * If expression doesn't returns any value (return type is void) 
	 * - <code>null</code> will be returned as a result.
	 * If return type contains some type parameters - they also will
	 * be returned in java manner (using triangle brackets) 
	 * @throws IOException in case of IO problems (unable to read sources file,
	 * unable to find source, etc.)
	 * @throws ParseException in case of either expression is 
	 * invalid or either current class or related ones have 
	 * syntax error(s).
	 */
	public static String evaluateReturnTypeOfExpression(
			TokenChain<? extends Token> iterator,
			String className,SourcesLoader loader,
			int start,int end) throws ParseException, IOException{
		int position = iterator.index();
		if(loader==null){
			loader = new UniversalSourcesLoader();
		}
		try{
			JavaClassDescriptor scope = getClassDescriptor(className,loader);
			ExpressionEvaluator evaluator = new ExpressionEvaluator(
					start,end,iterator,scope,loader);
			evaluator.evaluateReturnTypeOfExpression();
			return evaluator.getClassName();
		}
		finally{
			iterator.moveTo(position);
		}
	}
	/**
	 * This class is used to evaluate simple expressions.
	 * If some complex expression is passed - it will be split onto 
	 * simple ones and child {@link ExpressionEvaluator} will be created for
	 * each subexpression.
	 * 
	 * @author alex.bereznevatiy@gmail.com
	 */
	private static class ExpressionEvaluator{
		private TokenChain<? extends Token> iterator;
		private int start;
		private int end;
		private JavaClassDescriptor scope;
		private JavaClassDescriptor scopeRoot;
		private SourcesLoader loader;
		
		ExpressionEvaluator(int start,int end,
			TokenChain<? extends Token> iterator, 
			JavaClassDescriptor scope,
			SourcesLoader loader){
			this.iterator = iterator;
			this.start = start;
			this.end = end;
			this.scope = scope;
			this.scopeRoot = scope;
			this.loader = loader;
		}

		String getClassName() {
			if(scope==null){
				return null;
			}
			return scope.getClassName();
		}

		void evaluateReturnTypeOfExpression() 
				throws ParseException, IOException {
			iterator.moveTo(start);
			while(iterator.index()<end){
				scope = evaluateReturnTypeOfExpressionImpl();
			}
		}
	
		private JavaClassDescriptor evaluateReturnTypeOfExpressionImpl() 
				throws ParseException, IOException {
			if (iterator.index() >= end) {
				return scope;
			}
			if (scope == null) {
				throw new ParseException(
					"Unable to evaluate expression because subexpression "+
					getCurrentSubexpression()+" returning void");
			}
			String token = iterator.current().getToken();
			if (token == null || token.length() == 0) {
				iterator.next();
				return evaluateReturnTypeOfExpressionImpl();
			}
			if (token.equals("(")) {
				return evaluateReturnTypeOfSubexpression();
			}
			if (token.equals(")")) {
				skipCastSubExpression();
				return evaluateReturnTypeOfExpressionImpl();
			}
			if (token.equals("?")) {
				return evaluateTernaryOperatorReturnType();
			}
			if (token.equals("new")) {
				return evaluateNewStatementReturnType();
			}
			if (token.equals("[")) {
				return evaluateArrayElementReturnType();
			}
			if (isMath(token)) {
				iterator.next();
				return scope;
			}
			if (token.equals(".")) {
				if (iterator.index() == start) {
					throw new ParseException(
						"Expression should begin with java indefier");
				}
				token = iterator.next().getToken();
			}
			if (Name.isJavaName(token)) {
				return evaluateIdentifierReturnType(token);
			}
			return evaluateLiteralReturnType();
		}
	
		private boolean isMath(String token) {
			if(token.length()==1||
				token.length()==2){
				char c = token.charAt(0);
				return c=='+'||
					c=='-'||c=='*'||
					c=='%'||c=='/';
			}
			return false;
		}

		private JavaClassDescriptor evaluateReturnTypeOfSubexpression() 
				throws ParseException, IOException {
			int deep = 1;
			int exprStart = iterator.index()+1;
			int exprEnd = exprStart+1;
			while(deep>0){
				if(iterator.next().getToken().equals("(")){
					deep++;
				}
				else if(iterator.next().getToken().equals(")")){
					deep--;
				}
			}
			exprEnd = iterator.index();
			ExpressionEvaluator evaluator = new ExpressionEvaluator(
					exprStart, exprEnd, iterator, scopeRoot,loader);
			evaluator.evaluateReturnTypeOfExpression();
			return evaluator.scope;
		}
	
		private JavaClassDescriptor evaluateLiteralReturnType() 
				throws ParseException {
			String token = iterator.current().getToken();
			iterator.next();
			if (token.charAt(0) == '"') {
				return new JavaClassDescriptor(String.class);
			} else if (token.charAt(0)=='\'') {
				return new JavaClassDescriptor(Character.TYPE);
			}
			try {
				Integer.valueOf(token);
				return new JavaClassDescriptor(Integer.class);
			} catch(NumberFormatException ex) {
			}
			try {
				Long.valueOf(token);
				return new JavaClassDescriptor(Long.class);
			} catch(NumberFormatException ex) {
			}
			try {
				Double.valueOf(token);
				return new JavaClassDescriptor(Double.class);
			} catch(NumberFormatException ex) {
			}
			throw new ParseException("Unknown token at the end of subexpression: " + getCurrentSubexpression());
		}
	
		private JavaClassDescriptor evaluateIdentifierReturnType(
				String identifier) throws ParseException, IOException {
			if (identifier.equals("instanceof")) {
				iterator.next();
				iterator.next();
				return evaluateClassDescriptor(Boolean.TYPE.toString());
			}
			String idType = iterator.next().getToken();
			if (idType.equals("(")) {
				return evaluateMethodCallReturnType(identifier);
			}
			if (idType.equals(".") || idType.equals(")") || idType.equals("<") || idType.equals(",")) {
				return evaluatePropertyAccessReturnType(identifier);
			}
			if (idType.equals("[")) {
				if (!iterator.next().getToken().equals("]")) {
					throw new ParseException("Unknown expression: " + identifier + "["+iterator.current().getToken() +
							" at the end of subexpression: " + getCurrentSubexpression());
				}
				iterator.next();
				return evaluatePropertyAccessReturnType(identifier + "[]");
			}
			if (idType.equals("=")) {
				skipExpression(0);
				return scope;
			}
			throw new ParseException("Unknown token "+identifier+":"+idType+
					" at the end of subexpression: "+
					getCurrentSubexpression());
		}
	
		private JavaClassDescriptor evaluateTernaryOperatorReturnType() 
				throws ParseException, IOException {
			int start = iterator.next().getIndex();
			while(!iterator.next().getToken().equals(":"));
			ExpressionEvaluator evaluator = new ExpressionEvaluator(
					start, iterator.index(), iterator, scopeRoot, loader);
			evaluator.evaluateReturnTypeOfExpression();
			int deep = 1;
			while(deep>0){
				String tok = iterator.next().getToken();
				if(tok.equals("(")){
					deep++;
				}
				else if(tok.equals(")")||tok.equals(";")){
					deep--;
				}
			}
			return evaluator.scope;
		}

		private JavaClassDescriptor evaluatePropertyAccessReturnType(
				String identifier) throws ParseException, IOException {
			int index = (scope == scopeRoot ? iterator.index() : -1);
			JavaVariable var = scope.getScope(index)
					.getVariable(index, identifier);
			
			if(var != null){
				return evaluateClassDescriptor(var.getTypeName());
			}
			try {
				return evaluateTypeName(identifier);
			}
			catch(ParseException ex){
				throw new ParseException("Unable to get property with name " +
					identifier + " for expression " + getCurrentSubexpression());
			}
		}

		private JavaClassDescriptor evaluateTypeName(String identifier)
				throws ParseException {
			LinkedList<String> resultType = new LinkedList<String>();
			LinkedList<Token> positions = new LinkedList<Token>();
			resultType.add(readType(identifier));
			positions.add(iterator.current());
			
			while(iterator.current().getToken().equals(".")) {
				if (!iterator.hasNext() || iterator.current().getNext().getNext() == null) {
					break;
				}
				resultType.add(readType(iterator.next().getToken()));
				iterator.next();
				positions.add(iterator.current());
			}
			
			JavaClassDescriptor rez = null;
			while (rez == null) {
				try {
					String type = StringUtils.join(resultType, '.');
					resultType.removeLast();
					Token pos = positions.removeLast();
					rez = evaluateClassDescriptor(scope.getCompilationUnit().resolveType(type));
					iterator.moveTo(pos.getIndex());
				} catch (ParseException ex) {
					if (resultType.isEmpty()) {
						throw ex;
					}
				}
			}
			
			if (iterator.current().getToken().equals(")")) {
				skipCastSubExpression();
				return rez;
			}
			return rez;
		}

		private String readType(String identifier) {
			if(!iterator.current().getToken().equals("<")){
				return identifier;
			}
			StringBuilder result = new StringBuilder(identifier);
			int deep = 1;
			while(deep>0){
				result.append(iterator.current().getToken());
				if(iterator.next().getToken().equals("<")){
					deep++;
				}
				else if(iterator.current().getToken().equals(">")){
					deep--;
				}
			}
			result.append(iterator.current().getToken());
			iterator.next();
			return result.toString();
		}
		
		private void skipCastSubExpression() {
			int index = iterator.index()+1;
			int deep = 0;
			while(iterator.next().getToken().equals(")"))deep++;
			if(iterator.current().getToken().equals(".")||
				iterator.current().getToken().equals("?")){
				iterator.moveTo(index);
				return;//this is not a cast
			}
			if(!skipExpression(deep)){
				iterator.moveTo(index);
			}
		}

		private boolean skipExpression(int deep) {
			while(iterator.index()<end){
				if(iterator.next().getToken().equals(")")||
					iterator.current().getToken().equals(";")){
					deep--;
				}
				else if(iterator.current().getToken().equals("(")){
					deep++;
				}
				if(deep<0){
					iterator.next();
					return true;
				}
			}
			return false;
		}

		private JavaClassDescriptor evaluateNewStatementReturnType() 
				throws ParseException, IOException {
			StringBuilder type = new StringBuilder();
			while(!iterator.next().getToken().equals("(")&&
					!iterator.current().getToken().equals("[")){
				checkIteratorRange();
				type.append(iterator.current().getToken());
			}
			int deep = 1;
			final String open = iterator.current().getToken();
			final String close = open.equals("(")?")":"]";
			while(deep>0){
				checkIteratorRange();
				if(iterator.next().getToken().equals(open)){
					deep++;
				}
				else if(iterator.current().getToken().equals(close)){
					deep--;
				}
			}
			if(open.equals("(")){
				iterator.next();
				return evaluateClassDescriptor(type.toString());
			}
			type.append("[]");
			if(iterator.next().getToken().equals("{")){
				deep = 1;
				while(deep>0){
					checkIteratorRange();
					if(iterator.next().getToken().equals("{")){
						deep++;
					}
					else if(iterator.current().getToken().equals("}")){
						deep--;
					}
				}
				iterator.next();
			}
			return evaluateClassDescriptor(type.toString());
		}

		private void checkIteratorRange() throws ParseException {
			if (iterator.index() >= end ){
				throw new ParseException(
					"Expression is not valid:"+
					getCurrentSubexpression());
			}
		}
		
		private JavaClassDescriptor evaluateMethodCallReturnType(
				String identifier) throws ParseException, IOException {
			List<String> params = getMethodParametersTypes();
			JavaMethod method = scope.getCompilationUnit()
				.getMethod(identifier, params.toArray(
						new String[params.size()]));
			iterator.next();
			if(method==null){
				throwMethodNotFoundException(identifier, params);
			}
			return evaluateClassDescriptor(method.getReturnType());
		}

		private void throwMethodNotFoundException(String identifier,
				List<String> params) throws ParseException {
			StringBuilder message = new StringBuilder();
			message.append("Unable to get method ");
			message.append(identifier);
			message.append('(');
			Iterator<String> iterator = params.iterator();
			while(iterator.hasNext()){
				message.append(iterator.next());
				if(iterator.hasNext()){
					message.append(',');
				}
			}
			message.append(") on type ");
			message.append(scope.getClassName());
			message.append(". Expression is ");
			message.append(getCurrentSubexpression());
			throw new ParseException(message.toString());
		}

		private List<String> getMethodParametersTypes() throws ParseException,
				IOException {
			int deep = 1;
			List<String> params = new LinkedList<String>();
			int start = iterator.index()+1;
			while(deep>0){
				checkIteratorRange();
				if(iterator.next().getToken().equals("(")){
					deep++;
				}
				if(iterator.current().getToken().equals(")")){
					deep--;
				}
				if((deep==0&&iterator.index()>start)||(deep==1&&iterator
					.current().getToken().equals(","))){
					int end = iterator.index();
					ExpressionEvaluator evaluator = new ExpressionEvaluator(
						start, end, iterator, scopeRoot, loader);
					evaluator.evaluateReturnTypeOfExpression();
					params.add(evaluator.getClassName());
					start = end+1;
				}
			}
			return params;
		}
		
		private JavaClassDescriptor evaluateArrayElementReturnType() throws ParseException {
			if(!scope.isArray()){
				throw new ParseException("Return type of expression "+
						getCurrentSubexpression()+" is not an array");
			}
			while(!iterator.next().getToken().equals("]"));
			iterator.next();
			return scope.getArrayElement();
		}

		private JavaClassDescriptor evaluateClassDescriptor(String type)
				throws ParseException {
			if (type == null || type.length() == 0 || type.equals("void")) {
				return null;
			}
			type = Name.getRawName(type);
			if (type.charAt(0) == '[') {
				type = normalizeArray(type);
			}
			try {
				return getClassDescriptor(type, loader);
			} catch(IOException ex){
			}
			try {
				return new JavaClassDescriptor(resolveRuntimeClass(type));
			} catch(ClassNotFoundException ex) {
				throw new ParseException("Unable to find class ["+
					type+"] for subexpression "+getCurrentSubexpression());
			}
		}
		/**
		 * TODO: this method is ugly workaround
		 * right now I have no time for this.
		 * But for future - get rid of all type[] array
		 * signatures and use default signatures.
		 */
		private String normalizeArray(String type) {
			Class<?> cls = SystemClassLoader.getArrayType(type);
			if(cls!=null){
				type = cls.getComponentType().getName();
			}
			else {
				type = type.substring(2,type.length()-1);
			}
			return type+"[]";
		}

		private Class<?> resolveRuntimeClass(String type)
				throws ClassNotFoundException {
			int index = (scope == scopeRoot ? iterator.index() : -1);
			if(type.endsWith("[]")){
				type = type.substring(0,type.length()-2);
				type = scope.getScope(index).resolveType(type);
				type = SystemClassLoader.getArrayTypeName(type);
			}
			else {
				type = scope.getScope(index).resolveType(type);
			}
			if(SystemClassLoader.isPrimitiveClass(type)){
				return SystemClassLoader.getPrimitiveClass(type);
			}
			return Class.forName(type);
		}
	
		private String getCurrentSubexpression() {
			StringBuilder subexpr = new StringBuilder();
			int index = iterator.index();
			iterator.moveTo(start);
			String debug = iterator.current().getDebugInfo();
			subexpr.append('\'');
			subexpr.append(iterator.current().getRawToken());
			while(iterator.index()<=index){
				if (!iterator.hasNext()) {
					subexpr.append("[unable to get subexpression tail]");
					break;
				}
				subexpr.append(iterator.next().getRawToken());
			}
			subexpr.append('\'');
			subexpr.append(" for class ");
			subexpr.append(scopeRoot.getClassName());
			subexpr.append(debug);
			return subexpr.toString();
		}
	}
}
